/* eslint no-mixed-operators: 0 */
odoo.define('web_diagram_plus.Graph', function (require) {
    "use strict";

    var Vec2D = require('web_diagram_plus.Vec2D');

    var Vec2 = Vec2D.Vec2;
    var BRect = Vec2D.BRect;
    var BEllipse = Vec2D.BEllipse;

    // This serves as the end of an edge when creating a link
    function EdgeEnd (pos_x, pos_y) {
        this.x = pos_x;
        this.y = pos_y;

        this.get_pos = function () {
            return new Vec2(this.x, this.y);
        };
    }

    // A close button,
    // if entity_type == "node":
    // GraphNode.destruction_callback(entity) is called where entity is a node.
    // If it returns true the node and all connected edges are destroyed.
    // if entity_type == "edge":
    // GraphEdge.destruction_callback(entity) is called where entity is an edge
    // If it returns true the edge is destroyed
    // pos_x,pos_y is the relative position of the close button to the
    // entity position (entity.get_pos())

    function CloseButton (graph, entity, entity_type, pos_x, pos_y) {
        var self = this;
        var visible = false;
        var close_button_radius = graph.style.close_button_radius || 8;
        var close_circle = graph.r.circle(
            entity.get_pos().x + pos_x,
            entity.get_pos().y + pos_y,
            close_button_radius);
        // The outer gray circle
        close_circle.attr(
            {
                'opacity': 0,
                'fill': graph.style.close_button_color || "black",
                'cursor': 'pointer',
                'stroke': 'none',
            }
        );
        close_circle.transform(graph.get_transform());
        graph.set_scrolling(close_circle);

        // The 'x' inside the circle
        var close_label = graph.r.text(
            entity.get_pos().x + pos_x, entity.get_pos().y + pos_y, "x");
        close_label.attr(
            {
                'fill': graph.style.close_button_x_color || "white",
                'font-size': close_button_radius,
                'cursor': 'pointer',
            }
        );

        close_label.transform(graph.get_transform());
        graph.set_scrolling(close_label);

        // The dummy_circle is used to catch events, and avoid hover in/out
        // madness between the 'x' and the button
        var dummy_circle = graph.r.circle(
            entity.get_pos().x + pos_x,
            entity.get_pos().y + pos_y,
            close_button_radius);
        dummy_circle.attr(
            {
                'opacity':1,
                'fill': 'transparent',
                'stroke':'none',
                'cursor':'pointer',
            }
        );
        dummy_circle.transform(graph.get_transform());
        graph.set_scrolling(dummy_circle);

        this.get_pos = function () {
            return entity.get_pos().add_xy(pos_x, pos_y);
        };

        this.update_pos = function () {
            var pos = self.get_pos();
            close_circle.attr({'cx':pos.x, 'cy':pos.y});
            dummy_circle.attr({'cx':pos.x, 'cy':pos.y});
            close_label.attr({'x':pos.x, 'y':pos.y});
        };

        function hover_in () {
            if (!visible) {
                return;
            }
            close_circle.animate(
                {'r': close_button_radius * 1.5}, 300, 'elastic');
            dummy_circle.animate(
                {'r': close_button_radius * 1.5}, 300, 'elastic');
        }
        function hover_out () {
            if (!visible) {
                return;
            }
            close_circle.animate(
                {'r': close_button_radius}, 400, 'linear');
            dummy_circle.animate(
                {'r': close_button_radius}, 400, 'linear');
        }
        dummy_circle.hover(hover_in, hover_out);
        close_circle.hover(hover_in, hover_out);
        close_label.hover(hover_in, hover_out);

        function click_action () {
            if (!visible) {
                return;
            }

            close_circle.attr({'r': close_button_radius * 2});
            dummy_circle.attr({'r': close_button_radius * 2});
            close_circle.animate({'r': close_button_radius}, 400, 'linear');
            dummy_circle.animate({'r': close_button_radius}, 400, 'linear');

            if (entity_type === "node") {
                // eslint-disable-next-line no-use-before-define
                Promise.resolve(GraphNode.destruction_callback(entity)).then(
                    function () {
                        entity.remove();
                    }
                );
            } else if (entity_type === "edge") {
                // eslint-disable-next-line no-use-before-define
                Promise.resolve(GraphEdge.destruction_callback(entity)).then(
                    function () {
                        entity.remove();
                    }
                );
            }
        }
        dummy_circle.click(click_action);
        close_circle.click(click_action);
        close_label.click(click_action);

        this.show = function () {
            if (!visible) {
                close_circle.animate({'opacity': 1}, 100, 'linear');
                close_label.animate({'opacity': 1}, 100, 'linear');
                visible = true;
            }
        };
        this.hide = function () {
            if (visible) {
                close_circle.animate({'opacity': 0}, 100, 'linear');
                close_label.animate({'opacity': 0}, 100, 'linear');
                visible = false;
            }
        };
        // Destroy this object and remove it from the graph
        this.remove = function () {
            if (visible) {
                visible = false;
                close_circle.animate({'opacity': 0}, 100, 'linear');
                close_label.animate(
                    {'opacity': 0}, 100, 'linear', self.remove);
            } else {
                close_circle.remove();
                close_label.remove();
                dummy_circle.remove();
            }
        };
    }

    // Connectors are start and end point of edge creation drags.
    function Connector (graph, node, pos_x, pos_y) {
        var visible = false;
        var conn_circle = graph.r.circle(
            node.get_pos().x + pos_x, node.get_pos().y + pos_y, 4);
        conn_circle.attr(
            {
                'opacity': 0,
                'fill': graph.style.node_outline_color,
                'stroke': 'none',
            }
        );
        conn_circle.transform(graph.get_transform());
        graph.set_scrolling(conn_circle);

        var self = this;

        this.update_pos = function () {
            conn_circle.attr(
                {
                    'cx':node.get_pos().x + pos_x,
                    'cy':node.get_pos().y + pos_y,
                }
            );
        };
        this.get_pos = function () {
            return new node.get_pos().add_xy(pos_x, pos_y);
        };
        this.remove = function () {
            conn_circle.remove();
        };
        function hover_in () {
            if (!visible) {
                return;
            }
            conn_circle.animate({'r': 8}, 300, 'elastic');
            if (graph.creating_edge) {
                graph.target_node = node;
                conn_circle.animate(
                    {
                        'fill': graph.style.connector_active_color,
                        'stroke': graph.style.node_outline_color,
                        'stroke-width': graph.style.node_selected_width,
                    },
                    100,
                    'linear'
                );
            }
        }
        function hover_out () {
            if (!visible) {
                return;
            }
            conn_circle.animate(
                {
                    'r': graph.style.connector_radius,
                    'fill':graph.style.node_outline_color,
                    'stroke':'none',
                },
                400,
                'linear'
            );
            graph.target_node = null;
        }
        conn_circle.hover(hover_in, hover_out);

        var drag_down = function () {
            if (!visible) {
                return;
            }
            self.ox = conn_circle.attr("cx");
            self.oy = conn_circle.attr("cy");
            self.edge_start = new EdgeEnd(self.ox, self.oy);
            self.edge_end = new EdgeEnd(self.ox, self.oy);
            // eslint-disable-next-line no-use-before-define
            self.edge_tmp = new GraphEdge(
                graph, '', self.edge_start, self.edge_end, true);
            graph.creating_edge = true;
        };
        var drag_move = function (dx, dy) {
            if (!visible) {
                return;
            }
            self.edge_end.x = self.ox + dx;
            self.edge_end.y = self.oy + dy;
            self.edge_tmp.update();
        };
        var drag_up = function () {
            if (!visible) {
                return;
            }
            graph.creating_edge = false;
            self.edge_tmp.remove();
            if (graph.target_node) {
                // eslint-disable-next-line no-use-before-define
                var edge_prop = GraphEdge.creation_callback(
                    node, graph.target_node);
                if (edge_prop) {
                    // eslint-disable-next-line no-use-before-define
                    var new_edge = new GraphEdge(
                        graph, edge_prop.label, node, graph.target_node);
                    // eslint-disable-next-line no-use-before-define
                    GraphEdge.new_edge_callback(new_edge);
                }
            }
        };
        conn_circle.drag(drag_move, drag_down, drag_up);

        function show () {
            if (!visible) {
                conn_circle.animate({'opacity': 1}, 100, 'linear');
                visible = true;
            }
        }
        function hide () {
            if (visible) {
                conn_circle.animate({'opacity': 0}, 100, 'linear');
                visible = false;
            }
        }
        this.show = show;
        this.hide = hide;
    }

    // Creates a new graph on raphael document r.
    // style is a dictionary containing the style definitions
    // viewport (optional) is the dom element representing the viewport of the
    // graph.
    // It is used to prevent scrolling to scroll the graph outside the
    // viewport.

    function Graph (r, style, viewport) {
        var self = this;
        // List of all nodes in the graph
        var nodes = [];
        // List of all edges in the graph
        var edges = [];
        // The graph[n1.uid][n2.uid] -> list of all edges from n1 to n2
        var graph = {};
        // The links[n.uid] -> list of all edges from or to n
        var links = {};
        // All nodes and edges have an uid used to order their display when
        // they are curved
        var uid = 1;
        // The selected entity (node or edge)
        var selected_entity = null;

        // True if we are dragging a new edge onto a node
        self.creating_edge = false;
        // This holds the target node when creating an edge and hovering a
        // connector
        self.target_node = null;
        // The raphael instance
        self.r = r;
        // Definition of the colors, spacing, fonts, ... used by the elements
        self.style = style;
        // Global translation coordinate
        var tr_x = 0, tr_y = 0;

        var background = r.rect(0, 0, '100%', '100%').attr(
            {'fill': 'white', 'stroke': 'none', 'opacity': 0, 'cursor': 'move'}
        );

        // Return the global transform of the scene
        this.get_transform = function () {
            return "T" + tr_x + "," + tr_y;
        };

        // Translate every element of the graph except the background.
        // Elements inserted in the graph after a translate_all() must manually
        // apply transformation via get_transform()
        var translate_all = function (dx, dy) {
            tr_x += dx;
            tr_y += dy;
            var tstr = self.get_transform();

            r.forEach(function (el) {
                if (el !== background) {
                    el.transform(tstr);
                }
            });
        };
        // Returns {minx, miny, maxx, maxy},
        // the translated bounds containing all nodes
        var get_bounds = function () {
            var minx = Number.MAX_VALUE;
            var miny = Number.MAX_VALUE;
            var maxx = Number.MIN_VALUE;
            var maxy = Number.MIN_VALUE;

            for (var i = 0; i < nodes.length; i++) {
                var pos = nodes[i].get_pos();
                minx = Math.min(minx, pos.x);
                miny = Math.min(miny, pos.y);
                maxx = Math.max(maxx, pos.x);
                maxy = Math.max(maxy, pos.y);
            }

            minx = minx - style.node_size_x / 2 + tr_x;
            miny = miny - style.node_size_y / 2 + tr_y;
            maxx = maxx + style.node_size_x / 2 + tr_x;
            maxy = maxy + style.node_size_y / 2 + tr_y;

            return {minx:minx, miny:miny, maxx:maxx, maxy:maxy};

        };
        // Returns false if the translation dx,dy of the viewport
        // hides the graph (with optional margin)
        var translation_respects_viewport = function (dx, dy, margin) {
            if (!viewport) {
                return true;
            }
            var margin_norm = margin || 0;
            var b = get_bounds();
            var width = viewport.offsetWidth;
            var height = viewport.offsetHeight;

            if (dy < 0 && b.maxy + dy < margin_norm ||
                dy > 0 && b.miny + dy > height - margin_norm ||
                dx < 0 && b.maxx + dx < margin_norm ||
                dx > 0 && b.minx + dx > width - margin_norm) {
                return false;
            }

            return true;
        };
        // Adds a mousewheel event callback to raph_element that scrolls the
        // viewport
        this.set_scrolling = function (raph_element) {
            $(raph_element.node).bind('mousewheel', function (event, delta) {
                var dy = delta * 20;
                if (
                    translation_respects_viewport(0, dy, style.viewport_margin)
                ) {
                    translate_all(0, dy);
                }
            });
        };
        // eslint-disable-next-line init-declarations
        var px, py;
        // Graph translation when background is dragged
        var bg_drag_down = function () {
            px = py = 0;
        };
        var bg_drag_move = function (x, y) {
            var dx = x - px;
            var dy = y - py;
            px = x;
            py = y;
            if (translation_respects_viewport(dx, dy, style.viewport_margin)) {
                translate_all(dx, dy);
            }
        };
        // eslint-disable-next-line no-empty-function
        var bg_drag_up = function () {};
        background.drag(bg_drag_move, bg_drag_down, bg_drag_up);

        this.set_scrolling(background);

        // Adds a node to the graph and sets its uid.
        this.add_node = function (n) {
            nodes.push(n);
            n.uid = uid++;
        };

        // Return the list of all nodes in the graph
        this.get_node_list = function () {
            return nodes;
        };

        // Adds an edge to the graph and sets its uid
        this.add_edge = function (n1, n2, e) {
            edges.push(e);
            e.uid = uid++;
            if (!graph[n1.uid]) {
                graph[n1.uid] = {};
            }
            if (!graph[n1.uid][n2.uid]) {
                graph[n1.uid][n2.uid] = [];
            }
            if (!links[n1.uid]) {
                links[n1.uid] = [];
            }
            if (!links[n2.uid]) {
                links[n2.uid] = [];
            }
            graph[n1.uid][n2.uid].push(e);
            links[n1.uid].push(e);
            if (n1 !== n2) {
                links[n2.uid].push(e);
            }
        };

        // Removes an edge from the graph
        this.remove_edge = function (edge) {
            edges = _.without(edges, edge);
            var n1 = edge.get_start();
            var n2 = edge.get_end();
            links[n1.uid] = _.without(links[n1.uid], edge);
            links[n2.uid] = _.without(links[n2.uid], edge);
            graph[n1.uid][n2.uid] = _.without(graph[n1.uid][n2.uid], edge);
            if (selected_entity === edge) {
                selected_entity = null;
            }
        };
        // Removes a node and all connected edges from the graph
        this.remove_node = function (node) {
            var linked_edges = self.get_linked_edge_list(node);
            for (var i = 0; i < linked_edges.length; i++) {
                linked_edges[i].remove();
            }
            nodes = _.without(nodes, node);

            if (selected_entity === node) {
                selected_entity = null;
            }
        };

        // Return the list of edges from n1 to n2
        this.get_edge_list = function (n1, n2) {
            var list = [];
            if (!graph[n1.uid]) {
                return list;
            }
            if (!graph[n1.uid][n2.uid]) {
                return list;
            }
            return graph[n1.uid][n2.uid];
        };
        // Returns the list of all edge connected to n
        this.get_linked_edge_list = function (n) {
            if (!links[n.uid]) {
                return [];
            }
            return links[n.uid];
        };
        // Return a curvature index so that all edges connecting n1, n2 have
        // different curvatures
        this.get_edge_curvature = function (n1, n2, e) {
            var el_12 = this.get_edge_list(n1, n2);
            var c12 = el_12.length;
            var el_21 = this.get_edge_list(n2, n1);
            var c21 = el_21.length;
            if (c12 + c21 === 1) {
                // Only one edge
                return 0;
            }
            var index = 0;
            for (var i = 0; i < c12; i++) {
                if (el_12[i].uid < e.uid) {
                    index++;
                }
            }
            if (c21 === 0) {
                // All edges in the same direction
                return index - (c12-1) / 2.0;
            }
            return index + 0.5;
        };

        // Returns the angle in degrees of the edge loop.
        // We do not support more than 8 loops on one node
        this.get_loop_angle = function (n, e) {

            // The 8 angles where we can put the loops
            var slots = [];
            for (var angle = 0; angle < 360; angle += 45) {
                slots.push(Vec2.new_polar_deg(1, angle));
            }

            // We assign to each slot a score. The higher the score,
            // the closer it is to other edges.
            var links_ = this.get_linked_edge_list(n);
            for (var i = 0; i < links_.length; i++) {
                var edge = links_[i];
                if (!edge.is_loop || edge.is_loop()) {
                    continue;
                }
                var end = edge.get_end();
                if (end === n) {
                    end = edge.get_start();
                }
                var dir = end.get_pos().sub(n.get_pos()).normalize();
                for (var s = 0; s < slots.length; s++) {
                    var score = slots[s].dot(dir);
                    if (score < 0) {
                        score = -0.2 * Math.pow(score, 2);
                    } else {
                        score = Math.pow(score, 2);
                    }
                    // eslint-disable-next-line no-negated-condition
                    if (!slots[s].score) {
                        slots[s].score = score;
                    } else {
                        slots[s].score += score;
                    }
                }
            }
            // We want the loops with lower uid to get
            // the slots with the lower score
            slots.sort(function (a, b) {
                return a.score < b.score ? -1: 1;
            });

            var index = 0;
            for (var j = 0; j < links_.length; j++) {
                edge = links_[j];
                if (!edge.is_loop || !edge.is_loop()) {
                    continue;
                }
                if (edge.uid < e.uid) {
                    index++;
                }
            }
            index %= slots.length;

            return slots[index].angle_deg();
        };

        // Selects a node or an edge and deselects everything else
        this.select = function (entity) {
            if (selected_entity) {
                if (selected_entity === entity) {
                    return;
                }
                if (selected_entity.set_not_selected) {
                    selected_entity.set_not_selected();
                }
                selected_entity = null;
            }
            selected_entity = entity;
            if (entity && entity.set_selected) {
                entity.set_selected();
            }
        };
    }

    // Creates a new Graph Node on Raphael document r,
    // centered on [pos_x,pos_y], with label 'label',
    // and of type 'circle' or 'rect', and of color 'color'
    function GraphNode (graph, pos_x, pos_y, label, type, color, color_label) {
        var self = this;
        var r = graph.r;
        var sy = graph.style.node_size_y;
        var sx = graph.style.node_size_x;
        var node_fig = null;
        var selected = false;
        this.connectors = [];
        this.close_button = null;
        this.uid = 0;

        graph.add_node(this);

        if (type === 'circle') {
            node_fig = r.ellipse(pos_x, pos_y, sx / 2, sy / 2);
        } else {
            node_fig = r.rect(pos_x - sx / 2, pos_y - sy / 2, sx, sy);
        }
        node_fig.attr(
            {
                'fill': color,
                'stroke': graph.style.node_outline_color,
                'stroke-width': graph.style.node_outline_width,
                'cursor': 'pointer',
            }
        );
        node_fig.transform(graph.get_transform());
        graph.set_scrolling(node_fig);

        var node_label = r.text(pos_x, pos_y, label);
        node_label.attr(
            {
                'fill': color_label,
                'font-size': graph.style.node_label_font_size,
                'cursor': 'pointer',
            }
        );
        node_label.transform(graph.get_transform());
        graph.set_scrolling(node_label);

        // Redraws all edges linked to this node
        var update_linked_edges = function () {
            var edges = graph.get_linked_edge_list(self);
            for (var i = 0; i < edges.length; i++) {
                edges[i].update();
            }
        };

        // Sets the center position of the node
        var set_pos = function (pos) {
            if (type === 'circle') {
                node_fig.attr({'cx': pos.x, 'cy': pos.y});
            } else {
                node_fig.attr({'x': pos.x - sx / 2, 'y': pos.y - sy / 2});
            }
            node_label.attr({'x': pos.x, 'y': pos.y});
            for (var i = 0; i < self.connectors.length; i++) {
                self.connectors[i].update_pos();
            }
            if (self.close_button) {
                self.close_button.update_pos();
            }
            update_linked_edges();
        };
        // Returns the figure used to draw the node
        var get_fig = function () {
            return node_fig;
        };
        // Returns the center coordinates
        var get_pos = function () {
            if (type === 'circle') {
                return new Vec2(node_fig.attr('cx'), node_fig.attr('cy'));
            }
            return new Vec2(
                node_fig.attr('x') + sx / 2,
                node_fig.attr('y') + sy / 2
            );
        };
        // Return the label string
        var get_label = function () {
            return node_label.attr("text");
        };
        // Sets the label string
        var set_label = function (text) {
            node_label.attr({'text': text});
        };
        var get_bound = function () {
            if (type === 'circle') {
                return new BEllipse(get_pos().x, get_pos().y, sx / 2, sy / 2);
            }
            return BRect.new_centered(get_pos().x, get_pos().y, sx, sy);
        };
        // Selects this node and deselects all other nodes
        var set_selected = function () {
            if (!selected) {
                selected = true;
                node_fig.attr(
                    {
                        'stroke': graph.style.node_selected_color,
                        'stroke-width': graph.style.node_selected_width,
                    }
                );
                if (!self.close_button) {
                    self.close_button = new CloseButton(
                        graph, self, "node", sx / 2, - sy / 2);
                    self.close_button.show();
                }
                for (var i = 0; i < self.connectors.length; i++) {
                    self.connectors[i].show();
                }
            }
        };
        // Deselect this node
        var set_not_selected = function () {
            if (selected) {
                node_fig.animate(
                    {
                        'stroke': graph.style.node_outline_color,
                        'stroke-width': graph.style.node_outline_width,
                    },
                    100,
                    'linear'
                );
                if (self.close_button) {
                    self.close_button.remove();
                    self.close_button = null;
                }
                selected = false;
            }
            for (var i = 0; i < self.connectors.length; i++) {
                self.connectors[i].hide();
            }
        };
        var remove = function () {
            if (self.close_button) {
                self.close_button.remove();
            }
            for (var i = 0; i < self.connectors.length; i++) {
                self.connectors[i].remove();
            }
            graph.remove_node(self);
            node_fig.remove();
            node_label.remove();
        };

        this.set_pos = set_pos;
        this.get_pos = get_pos;
        this.set_label = set_label;
        this.get_label = get_label;
        this.get_bound = get_bound;
        this.get_fig = get_fig;
        this.set_selected = set_selected;
        this.set_not_selected = set_not_selected;
        this.update_linked_edges = update_linked_edges;
        this.remove = remove;

        // Select the node and play an animation when clicked
        var click_action = function () {
            if (type === 'circle') {
                node_fig.attr({'rx': sx / 2 + 3, 'ry': sy / 2 + 3});
                node_fig.animate({'rx': sx / 2, 'ry': sy / 2}, 500, 'elastic');
            } else {
                var cx = get_pos().x;
                var cy = get_pos().y;
                node_fig.attr(
                    {
                        'x': cx - sx / 2 - 3,
                        'y': cy - sy / 2 - 3,
                        'width': sx + 6,
                        'height': sy + 6,
                    }
                );
                node_fig.animate(
                    {
                        'x': cx - sx / 2,
                        'y': cy - sy / 2,
                        'width': sx,
                        'height':sy,
                    },
                    500,
                    'elastic'
                );
            }
            graph.select(self);
        };
        node_fig.click(click_action);
        node_label.click(click_action);

        // Move the node when dragged
        var drag_down = function () {
            this.opos = get_pos();
        };
        var drag_move = function (dx, dy) {
            // We disable labels when moving for performance reasons,
            // updating the label position is quite expensive
            // we put this here because drag_down is also called on
            // simple clicks ... and this causes unwanted flicker
            var edges = graph.get_linked_edge_list(self);
            for (var i = 0; i < edges.length; i++) {
                edges[i].label_disable();
            }
            if (self.close_button) {
                self.close_button.hide();
            }
            set_pos(this.opos.add_xy(dx, dy));
        };
        var drag_up = function () {
            // We re-enable the
            var edges = graph.get_linked_edge_list(self);
            for (var i = 0; i < edges.length; i++) {
                edges[i].label_enable();
            }
            if (self.close_button) {
                self.close_button.show();
            }
        };
        node_fig.drag(drag_move, drag_down, drag_up);
        node_label.drag(drag_move, drag_down, drag_up);

        // Allow the user to create edges by dragging onto the node
        function hover_in () {
            if (graph.creating_edge) {
                graph.target_node = self;
            }
        }
        function hover_out () {
            graph.target_node = null;
        }
        node_fig.hover(hover_in, hover_out);
        node_label.hover(hover_in, hover_out);

        function double_click () {
            GraphNode.double_click_callback(self);
        }
        node_fig.dblclick(double_click);
        node_label.dblclick(double_click);

        this.connectors.push(new Connector(graph, this, - sx / 2, 0));
        this.connectors.push(new Connector(graph, this, sx / 2, 0));
        this.connectors.push(new Connector(graph, this, 0, - sy / 2));
        this.connectors.push(new Connector(graph, this, 0, sy / 2));

        this.close_button = new CloseButton(
            graph, this, "node", sx / 2, - sy / 2);
    }

    GraphNode.double_click_callback = function (node) {
        // eslint-disable-next-line no-console
        console.log("double click from node:", node);
    };

    // This is the default node destruction callback.
    // It is called before the node is removed from the graph
    // and before the connected edges are destroyed
    // eslint-disable-next-line no-unused-vars
    GraphNode.destruction_callback = function (node) {
        return true;
    };

    // Creates a new edge with label 'label' from start to end.
    // Start and end must implement get_pos_*,
    // if tmp is true, the edge is not added to the graph, used for drag edges.
    // replace tmp == false by graph == null
    function GraphEdge (graph, label, start, end, tmp) {
        var self = this;
        var r = graph.r;
        // The 0 = straight, != 0 curved
        var curvature = 0;
        // Positions of the start and end point of the
        // line between start and end
        // eslint-disable-next-line init-declarations
        var s, e;
        // Position of the middle of the curve (bezier control point)
        // eslint-disable-next-line init-declarations
        var mc;
        // Control points of the cubic bezier for the loop edges
        // eslint-disable-next-line init-declarations
        var mc1, mc2;
        var elfs = graph.style.edge_label_font_size || 10;
        var label_enabled = true;
        // Unique id used to order the curved edges
        this.uid = 0;
        // The svg definition of the edge vector path
        var edge_path = "";
        var selected = false;

        if (!tmp) {
            graph.add_edge(start, end, this);
        }

        // Return the position of the label
        function get_label_pos (path) {
            var cpos = path.getTotalLength() * 0.5;
            var cindex = Math.abs(Math.floor(curvature));
            var mod = cindex % 3 * elfs * 3.1 - elfs * 0.5;
            var verticality = Math.abs(
                end.get_pos().sub(start.get_pos()).normalize().dot_xy(0, 1));
            verticality = Math.max(verticality - 0.5, 0) * 2;

            var lpos = path.getPointAtLength(cpos + mod * verticality);
            return new Vec2(lpos.x, lpos.y - elfs * (1 - verticality));
        }

        // Used by close_button
        this.get_pos = function () {
            // eslint-disable-next-line no-use-before-define
            if (!edge) {
                return start.get_pos().lerp(end.get_pos(), 0.5);
            }
            // eslint-disable-next-line no-use-before-define
            return get_label_pos(edge);
            // The var bbox = edge_label.getBBox(); Does not work... :(
            // return new Vec2(bbox.x + bbox.width, bbox.y);
        };

        // Straight line from s to e
        function make_line () {
            return "M" + s.x + "," + s.y + "L" + e.x + "," + e.y;
        }
        // Curved line from s to e by mc
        function make_curve () {
            return "M" + s.x + "," + s.y +
                   "Q" + mc.x + "," + mc.y + " " + e.x + "," + e.y;
        }
        // Curved line from s to e by mc1 mc2
        function make_loop () {
            return "M" + s.x + " " + s.y +
                   "C" + mc1.x + " " + mc1.y + " " + mc2.x + " " +
                   mc2.y + " " + e.x + " " + e.y;
        }

        // Computes new start and end line coordinates
        function update_curve () {
            // eslint-disable-next-line init-declarations
            var col;
            // eslint-disable-next-line no-negated-condition
            if (start !== end) {
                // eslint-disable-next-line no-negated-condition
                if (!tmp) {
                    curvature = graph.get_edge_curvature(start, end, self);
                } else {
                    curvature = 0;
                }
                s = start.get_pos();
                e = end.get_pos();

                // Middle of the line s->e
                mc = s.lerp(e, 0.5);
                var se = e.sub(s);
                se = se.normalize();
                se = se.rotate_deg(- 90);
                se = se.scale(curvature * graph.style.edge_spacing);
                mc = mc.add(se);

                if (start.get_bound) {
                    col = start.get_bound().collide_segment(s, mc);
                    if (col.length > 0) {
                        s = col[0];
                    }
                }
                if (end.get_bound) {
                    col = end.get_bound().collide_segment(mc, e);
                    if (col.length > 0) {
                        e = col[0];
                    }
                }
                // eslint-disable-next-line no-negated-condition
                if (curvature !== 0) {
                    edge_path = make_curve();
                } else {
                    edge_path = make_line();
                }
            } else {
                // The start == end
                var rad = graph.style.edge_loop_radius || 100;
                s = start.get_pos();
                e = end.get_pos();

                var r_ = Vec2.new_polar_deg(
                    rad, graph.get_loop_angle(start, self));
                mc = s.add(r_);
                var p = r_.rotate_deg(90);
                mc1 = mc.add(p.set_len(rad * 0.5));
                mc2 = mc.add(p.set_len(- rad * 0.5));

                if (start.get_bound) {
                    col = start.get_bound().collide_segment(s, mc1);
                    if (col.length > 0) {
                        s = col[0];
                    }
                    col = start.get_bound().collide_segment(e, mc2);
                    if (col.length > 0) {
                        e = col[0];
                    }
                }
                edge_path = make_loop();
            }
        }

        update_curve();
        var edge = r.path(edge_path).attr(
            {
                'stroke': graph.style.edge_color,
                'stroke-width': graph.style.edge_width,
                'arrow-end': 'block-wide-long',
                'cursor':'pointer',
            }
        ).insertBefore(graph.get_node_list()[0].get_fig());
        var labelpos = get_label_pos(edge);
        var edge_label = r.text(labelpos.x, labelpos.y - elfs, label).attr(
            {
                'fill': graph.style.edge_label_color,
                'cursor': 'pointer',
                'font-size': elfs,
            }
        );

        edge.transform(graph.get_transform());
        graph.set_scrolling(edge);

        edge_label.transform(graph.get_transform());
        graph.set_scrolling(edge_label);

        // Since we create an edge we need to recompute the edges that have the
        // same start and end positions as this one
        if (!tmp) {
            var edges_start = graph.get_linked_edge_list(start);
            var edges_end = graph.get_linked_edge_list(end);
            var edges = edges_start.length < edges_end.length
                ? edges_start : edges_end;
            for (var i = 0; i < edges.length; i ++) {
                if (edges[i] !== self) {
                    edges[i].update();
                }
            }
        }
        function label_enable () {
            if (!label_enabled) {
                label_enabled = true;
                edge_label.animate({'opacity': 1}, 100, 'linear');
                if (self.close_button) {
                    self.close_button.show();
                }
                self.update();
            }
        }
        function label_disable () {
            if (label_enabled) {
                label_enabled = false;
                edge_label.animate({'opacity': 0}, 100, 'linear');
                if (self.close_button) {
                    self.close_button.hide();
                }
            }
        }
        // Update the positions
        function update () {
            update_curve();
            edge.attr({'path':edge_path});
            if (label_enabled) {
                var labelpos_ = get_label_pos(edge);
                edge_label.attr({'x': labelpos_.x, 'y': labelpos_.y - 14});
            }
        }
        // Removes the edge from the scene, disconnects it from linked
        // nodes, destroy its drawable elements.
        function remove () {
            edge.remove();
            edge_label.remove();
            if (!tmp) {
                graph.remove_edge(self);
            }
            if (start.update_linked_edges) {
                start.update_linked_edges();
            }
            if (start !== end && end.update_linked_edges) {
                end.update_linked_edges();
            }
            if (self.close_button) {
                self.close_button.remove();
            }
        }

        this.set_selected = function () {
            if (!selected) {
                selected = true;
                edge.attr(
                    {
                        'stroke': graph.style.node_selected_color,
                        'stroke-width': graph.style.node_selected_width,
                    }
                );
                edge_label.attr({'fill': graph.style.node_selected_color});
                if (!self.close_button) {
                    self.close_button = new CloseButton(
                        graph, self, "edge", 0, 30);
                    self.close_button.show();
                }
            }
        };

        this.set_not_selected = function () {
            if (selected) {
                selected = false;
                edge.animate(
                    {
                        'stroke': graph.style.edge_color,
                        'stroke-width': graph.style.edge_width,
                    },
                    100,
                    'linear'
                );
                edge_label.animate(
                    {'fill': graph.style.edge_label_color},
                    100,
                    'linear'
                );
                if (self.close_button) {
                    self.close_button.remove();
                    self.close_button = null;
                }
            }
        };
        function click_action () {
            graph.select(self);
        }
        edge.click(click_action);
        edge_label.click(click_action);

        function double_click_action () {
            GraphEdge.double_click_callback(self);
        }

        edge.dblclick(double_click_action);
        edge_label.dblclick(double_click_action);

        this.label_enable = label_enable;
        this.label_disable = label_disable;
        this.update = update;
        this.remove = remove;
        this.is_loop = function () {
            return start === end;
        };
        this.get_start = function () {
            return start;
        };
        this.get_end = function () {
            return end;
        };
    }

    GraphEdge.double_click_callback = function (edge) {
        // eslint-disable-next-line no-console
        console.log("double click from edge:", edge);
    };

    // This is the default edge creation callback.
    // It is called before an edge is created.
    // It returns an object containing the properties of the edge.
    // If it returns null, the edge is not created.
    // eslint-disable-next-line no-unused-vars
    GraphEdge.creation_callback = function (start, end) {
        var edge_prop = {};
        edge_prop.label = 'new edge!';
        return edge_prop;
    };
    // This is is called after a new edge is created, with the new edge
    // as parameter
    // eslint-disable-next-line no-empty-function, no-unused-vars
    GraphEdge.new_edge_callback = function (new_edge) {};

    // This is the default edge destruction callback. It is called before
    // an edge is removed from the graph.
    // eslint-disable-next-line no-unused-vars
    GraphEdge.destruction_callback = function (edge) {
        return true;
    };

    // Returns a new string with the same content as str,
    // but with lines of maximum 'width' characters.
    // lines are broken on words, or
    // into words if a word is longer than 'width'
    function wordwrap (str, width) {
        // http://james.padolsey.com/javascript/wordwrap-for-javascript/
        var width_norm = width || 32;
        var cut = true;
        var brk = '\n';
        if (!str) {
            return str;
        }
        var regex = '.{1,' + width_norm + '}(\\s|$)' +
                    (cut ? '|.{' + width_norm + '}|.+$' : '|\\S+?(\\s|$)');
        return str.match(new RegExp(regex, 'g')).join(brk);
    }

    return {
        CuteGraphPlus: Graph,
        CuteNodePlus: GraphNode,
        CuteEdgePlus: GraphEdge,
        CuteGraphPlus_wordwrap: wordwrap,
    };

});
